package com.google.code.qsecurity.client.mvp;

import java.util.LinkedHashSet;
import java.util.Set;

import com.google.code.qsecurity.client.event.ForgottenPasswordEvent;
import com.google.code.qsecurity.client.event.LoginFormEvent;
import com.google.code.qsecurity.client.event.OfflineEvent;
import com.google.code.qsecurity.client.event.OnlineEvent;
import com.google.code.qsecurity.client.event.SecurityEvent;
import com.google.code.qsecurity.client.event.SecurityFormEvent;
import com.google.code.qsecurity.client.event.SecurityFormHandler;
import com.google.code.qsecurity.client.event.SignupFormEvent;
import com.google.code.qsecurity.client.event.StateChangeEvent;
import com.google.code.qsecurity.client.event.StateChangeHandler;
import com.google.code.qsecurity.client.event.VerificationFormEvent;
import com.google.gwt.activity.shared.AbstractActivity;
import com.google.gwt.activity.shared.Activity;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.event.shared.ResettableEventBus;
import com.google.gwt.event.shared.UmbrellaException;
import com.google.gwt.place.shared.PlaceChangeEvent;
import com.google.gwt.place.shared.PlaceChangeRequestEvent;
import com.google.gwt.user.client.ui.AcceptsOneWidget;
import com.google.gwt.user.client.ui.IsWidget;
import com.google.inject.Inject;

public class SecurityActivityManager implements SecurityFormHandler, StateChangeHandler {

	/**
	 * Wraps our real display to prevent an Activity from taking it over if it is
	 * not the currentActivity.
	 */
	private class ProtectedDisplay implements AcceptsOneWidget {
		private final Activity activity;

		ProtectedDisplay(Activity activity) {
			this.activity = activity;
		}

		public void setWidget(IsWidget view) {
			if (this.activity == SecurityActivityManager.this.currentActivity) {
				startingNext = false;
				showWidget(view);
			}
		}
	}

	private static final Activity NULL_ACTIVITY = new AbstractActivity() {
		public void start(AcceptsOneWidget panel, com.google.gwt.event.shared.EventBus eventBus) {
		}
	};

	private final SecurityActivityMapper mapper;

	private final EventBus eventBus;

	/*
	 * Note that we use the legacy class from com.google.gwt.event.shared, because
	 * we can't change the Activity interface.
	 */
	private final ResettableEventBus stopperedEventBus;

	private Activity currentActivity = NULL_ACTIVITY;

	private AcceptsOneWidget display;

	private boolean startingNext = false;

	private HandlerRegistration handlerRegistration;

	/**
	 * Create an ActivityManager. Next call {@link #setDisplay}.
	 * 
	 * @param mapper finds the {@link Activity} for a given
	 *          {@link com.google.gwt.place.shared.Place}
	 * @param eventBus source of {@link PlaceChangeEvent} and
	 *          {@link PlaceChangeRequestEvent} events.
	 */
	@Inject
	public SecurityActivityManager(SecurityActivityMapper mapper, EventBus eventBus) {
		this.mapper = mapper;
		this.eventBus = eventBus;
		this.stopperedEventBus = new ResettableEventBus(eventBus);
	}

	/**
	 * Deactivate the current activity, find the next one from our ActivityMapper,
	 * and start it.
	 * <p>
	 * The current activity's widget will be hidden immediately, which can cause
	 * flicker if the next activity provides its widget asynchronously. That can
	 * be minimized by decent caching. Perenially slow activities might mitigate
	 * this by providing a widget immediately, with some kind of "loading"
	 * treatment.
	 */
	public void process(SecurityEvent<?> event) {
		Activity nextActivity = getNextActivity(event);

		Throwable caughtOnStop = null;
		Throwable caughtOnCancel = null;
		Throwable caughtOnStart = null;

		if (nextActivity == null) {
			nextActivity = NULL_ACTIVITY;
		}

		if (currentActivity.equals(nextActivity)) {
			return;
		}

		if (startingNext) {
			// The place changed again before the new current activity showed its
			// widget
			caughtOnCancel = tryStopOrCancel(false);
			currentActivity = NULL_ACTIVITY;
			startingNext = false;
		} else if (!currentActivity.equals(NULL_ACTIVITY)) {
			showWidget(null);

			/*
			 * Kill off the activity's handlers, so it doesn't have to worry about
			 * them accidentally firing as a side effect of its tear down
			 */
			stopperedEventBus.removeHandlers();
			caughtOnStop = tryStopOrCancel(true);
		}

		currentActivity = nextActivity;

		if (currentActivity.equals(NULL_ACTIVITY)) {
			showWidget(null);
		} else {
			startingNext = true;
			caughtOnStart = tryStart();
		}

		if (caughtOnStart != null || caughtOnCancel != null || caughtOnStop != null) {
			Set<Throwable> causes = new LinkedHashSet<Throwable>();
			if (caughtOnStop != null) {
				causes.add(caughtOnStop);
			}
			if (caughtOnCancel != null) {
				causes.add(caughtOnCancel);
			}
			if (caughtOnStart != null) {
				causes.add(caughtOnStart);
			}

			throw new UmbrellaException(causes);
		}
	}

	/**
	 * Sets the display for the receiver, and has the side effect of starting or
	 * stopping its monitoring the event bus for place change events.
	 * <p>
	 * If you are disposing of an ActivityManager, it is important to call
	 * setDisplay(null) to get it to deregister from the event bus, so that it can
	 * be garbage collected.
	 * 
	 * @param display an instance of AcceptsOneWidget
	 */
	public void setDisplay(AcceptsOneWidget display) {
		boolean wasActive = (null != this.display);
		boolean willBeActive = (null != display);
		this.display = display;
		if (wasActive != willBeActive) {
			updateHandlers(willBeActive);
		}
	}

	private Activity getNextActivity(SecurityEvent<?> event) {
		if (display == null) {
			/*
			 * Display may have been nulled during PlaceChangeEvent dispatch. Don't
			 * bother the mapper, just return a null to ensure we shut down the
			 * current activity
			 */
			return null;
		}
		return mapper.getActivity(event);
	}

	private void showWidget(IsWidget view) {
		if (display != null) {
			display.setWidget(view);
		}
	}

	private Throwable tryStart() {
		Throwable caughtOnStart = null;
		try {
			/*
			 * Wrap the actual display with a per-call instance that protects the
			 * display from canceled or stopped activities, and which maintains our
			 * startingNext state.
			 */
			currentActivity.start(new ProtectedDisplay(currentActivity), stopperedEventBus);
		} catch (Throwable t) {
			caughtOnStart = t;
		}
		return caughtOnStart;
	}

	private Throwable tryStopOrCancel(boolean stop) {
		Throwable caughtOnStop = null;
		try {
			if (stop) {
				currentActivity.onStop();
			} else {
				currentActivity.onCancel();
			}
		} catch (Throwable t) {
			caughtOnStop = t;
		} finally {
			/*
			 * Kill off the handlers again in case it was naughty and added new ones
			 * during onstop or oncancel
			 */
			stopperedEventBus.removeHandlers();
		}
		return caughtOnStop;
	}

	private void updateHandlers(boolean activate) {
		if (activate) {
			final HandlerRegistration onlineReg = eventBus.addHandler(OnlineEvent.getType(), this);
			final HandlerRegistration offlineReg = eventBus.addHandler(OfflineEvent.getType(), this);
			final HandlerRegistration loginReg = eventBus.addHandler(LoginFormEvent.getType(), this);
			final HandlerRegistration signupReg = eventBus.addHandler(SignupFormEvent.getType(), this);
			final HandlerRegistration verificationReg = eventBus.addHandler(VerificationFormEvent.getType(), this);
			final HandlerRegistration forgottenPasswordReg = eventBus.addHandler(ForgottenPasswordEvent.getType(), this);

			this.handlerRegistration = new HandlerRegistration() {
				public void removeHandler() {
					onlineReg.removeHandler();
					offlineReg.removeHandler();
					loginReg.removeHandler();
					signupReg.removeHandler();
					verificationReg.removeHandler();
					forgottenPasswordReg.removeHandler();
				}
			};
		} else {
			if (handlerRegistration != null) {
				handlerRegistration.removeHandler();
				handlerRegistration = null;
			}
		}
	}

	@Override
	public void onStateChange(StateChangeEvent event) {
		process(event);
	}

	@Override
	public void onDisplay(SecurityFormEvent event) {
		process(event);
	}

}
